package cn.redragon.soa.config.web;

import static cn.redragon.soa.common.core.constant.AuthConstant.ACCESS_DENIED_REASON;

import brave.Tracer;
import cn.redragon.soa.common.core.exception.AmqpException;
import cn.redragon.soa.common.core.exception.BaseException;
import cn.redragon.soa.common.core.exception.ObjectNotFoundException;
import cn.redragon.soa.common.core.exception.ServiceException;
import cn.redragon.soa.common.core.exception.SystemError;
import cn.redragon.soa.common.core.web.ResponseDTO;
import com.fasterxml.jackson.core.JsonProcessingException;
import feign.codec.DecodeException;
import io.vavr.control.Try;
import java.lang.reflect.UndeclaredThrowableException;
import javax.persistence.EntityNotFoundException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.multipart.support.MissingServletRequestPartException;

@ControllerAdvice
@Component
@Slf4j
@ConditionalOnWebApplication
public class ControllerExceptionHandler {

    @Autowired(required = false)
    private Tracer tracer;

    @ExceptionHandler({ServiceException.class, AmqpException.class})
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public ResponseDTO<?> exceptionHandler(BaseException exception) {
        log.error(exception.toString(), exception);
        return ResponseDTO.build(exception.getCode(), exception.getData()).withTraceId(getTraceId());
    }

    @ExceptionHandler(DecodeException.class)
    @ResponseBody
    public ResponseEntity<ResponseDTO<?>> exceptionHandler(DecodeException exception) {
        if (BaseException.class.isAssignableFrom(exception.getCause().getClass())) {
            BaseException baseException = (BaseException) exception.getCause();
            log.error(baseException.toString(), exception);
            return
                ResponseEntity.ok(ResponseDTO.build(baseException.getCode(), baseException.getData()).withTraceId(getTraceId()));
        }
        log.error(exception.getMessage(), exception);

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ResponseDTO.fail(SystemError.SYSTEM_INTERNAL_ERROR).withTraceId(getTraceId()));
    }

    @ExceptionHandler(JsonProcessingException.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseDTO<Void> exceptionHandler(JsonProcessingException exception) {
        log.error(exception.getMessage(), exception);
        return ResponseDTO.fail(SystemError.INPUT_CONVERT_ERROR).withTraceId(getTraceId());
    }

    @ExceptionHandler({ObjectNotFoundException.class, EntityNotFoundException.class})
    @ResponseBody
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ResponseDTO<Void> exceptionHandler(RuntimeException exception) {
        log.error(exception.getMessage(), exception);
        return ResponseDTO.fail(SystemError.NOT_FOUND).withTraceId(getTraceId());
    }

    @ExceptionHandler({ServletRequestBindingException.class, TypeMismatchException.class, HttpMessageConversionException.class, BindException.class,
        MissingServletRequestParameterException.class, MethodArgumentNotValidException.class, IllegalArgumentException.class,
        ConstraintViolationException.class, HttpRequestMethodNotSupportedException.class, MissingServletRequestPartException.class})
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    @ResponseBody
    public ResponseDTO<Void> exceptionHandler(Exception e) {
        log.error(e.getMessage(), e);
        return ResponseDTO.fail(SystemError.INVALID_PARAM_ERROR).withTraceId(getTraceId());
    }

    @ExceptionHandler(AuthenticationException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ResponseDTO<Void> exceptionHandler(AuthenticationException exception) {
        log.error(exception.getMessage(), exception);
        return ResponseDTO.fail(SystemError.ACCESS_DENIED);
    }

    @ExceptionHandler(AccessDeniedException.class)
    @ResponseStatus(value = HttpStatus.FORBIDDEN, reason= ACCESS_DENIED_REASON)
    public ResponseDTO<Void> exceptionHandler(AccessDeniedException exception, HttpServletRequest request) {
        log.error("Access denied for URI {}", request.getRequestURI());
        log.error(exception.getMessage(), exception);
        return ResponseDTO.fail(SystemError.ACCESS_DENIED);

    }

    @ExceptionHandler(UndeclaredThrowableException.class)
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public ResponseDTO<Void> exceptionHandler(UndeclaredThrowableException e) {
        log.error(e.getUndeclaredThrowable().getMessage(), e);
        return ResponseDTO
            .fail(SystemError.SYSTEM_INTERNAL_ERROR).withTraceId(getTraceId());
    }

    @ExceptionHandler(Exception.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseDTO<Void> globalExceptionHandler(Exception exception) {
        log.error(exception.getMessage(), exception);
        return ResponseDTO.fail(SystemError.SYSTEM_INTERNAL_ERROR).withTraceId(getTraceId());
    }

    private String getTraceId() {
        return Try.of(() -> tracer.currentSpan().context().traceIdString()).getOrElse(() -> null);
    }
}
